/*
 * Copyright 2008 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package net.thick4.context;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Iterator;

import net.thick4.context.support.NoOperationMap;
import net.thick4.convention.TempConvention;
import net.thick4.deploy.DeploymentMode;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionReader;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.context.annotation.CommonAnnotationBeanPostProcessor;
import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.XmlWebApplicationContext;

/**
 * Hotdeploy対応の XmlWebApplicationContext です。 
 */
public class ReloadableXmlWebApplicationContext extends XmlWebApplicationContext {
		
	/**
	 * アプリケーションの基準ディレクトリーを指定するContextParameterのキー。 
	 */
	public static final String BASE_PACKAGE_KEY = "basePackage";
	
    /**
	 * コンテキストの再設定をおこなう {@link ReloadableContextReconfigurer} です。
	 */
	protected ReloadableContextReconfigurer contextReconfigure;

	/**
	 * 基準パッケージです。
	 */
	private String basePackage;
	
	/**
	 * デフォルトコンストラクタです。
	 */
	public ReloadableXmlWebApplicationContext() {
		super();
	}

	/**
	 * {@link BeanDefinitionReader} を初期化します。
	 * @param reader XML形式のBean定義ファイルを読み込み用の{@link BeanDefinitionReader}
	 */
	protected void initBeanDefinitionReader(XmlBeanDefinitionReader beanDefinitionReader) {
   		basePackage = getServletContext().getInitParameter(BASE_PACKAGE_KEY);
		
		super.initBeanDefinitionReader(beanDefinitionReader);
		
    	if (DeploymentMode.isDevelopmentMode()) {
			contextReconfigure = new ReloadableContextReconfigurer(getServletContext());
			contextReconfigure.registerXmlResources(beanDefinitionReader, getConfigLocations());
		}
	}
		
	/**
	 * Spring Bean定義ファイルを読み込みます。
	 * @see org.springframework.web.context.support.XmlWebApplicationContext#loadBeanDefinitions(org.springframework.beans.factory.xml.XmlBeanDefinitionReader)
	 */
	protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {
		String originalBasePackage = System.getProperty(BASE_PACKAGE_KEY);
		try {
			if (basePackage != null) {
				System.setProperty(BASE_PACKAGE_KEY, basePackage);
			}
			super.loadBeanDefinitions(reader);
    	} finally {
    		if (originalBasePackage != null) {
    			System.setProperty(BASE_PACKAGE_KEY, originalBasePackage);
    		} else {
    			System.clearProperty(BASE_PACKAGE_KEY);
    		}
    	}
    }

	/**
	 * BeanFactoryをカスタマイズします。
	 * HotdeployのためにBeanMetadataのキャッシュを無効に設定し、
	 * 設定読み込み時にクラスがキャッシュされないようにするために
	 * クラスローダーを入れ替えます。
	 * @param beanFactory カスタマイズ対象の {@link BeanFactory}
	 */
	protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
    	if (DeploymentMode.isDevelopmentMode()) {
    		TempConvention convention = new TempConvention();
    		convention.setBasePackages(basePackage);
    		
    		ReloadableClassLoader tempClassLoader = new ReloadableClassLoader(getClassLoader(), convention);
			setClassLoader(tempClassLoader);
			
			beanFactory.setCacheBeanMetadata(false);
    	}
    	
		super.prepareBeanFactory(beanFactory);
	}
	
	/**
	 * BeanPostProcessorの登録をおこなうと同時に、
	 * Hotdeployのために内部キャッシュを無効に設定します。
	 * @param beanFactory {@link BeanPostProcessor} を登録する {@link BeanFactory}
	 * @see AutowiredAnnotationBeanPostProcessor
	 * @see CommonAnnotationBeanPostProcessor
	 * @see PersistenceAnnotationBeanPostProcessor
	 */
	protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFactory) {
		super.registerBeanPostProcessors(beanFactory);
		
    	if (DeploymentMode.isDevelopmentMode()) {
			AbstractAutowireCapableBeanFactory factory = null;
			if (beanFactory instanceof AbstractAutowireCapableBeanFactory) {
				factory = (AbstractAutowireCapableBeanFactory) beanFactory;
			}
			
			if (factory != null) {
				Iterator it = factory.getBeanPostProcessors().iterator();
				while (it.hasNext()) {
					BeanPostProcessor processor = (BeanPostProcessor) it.next();
					if (processor instanceof AutowiredAnnotationBeanPostProcessor) {
						disableCache(AutowiredAnnotationBeanPostProcessor.class, processor, "candidateConstructorsCache");
						disableCache(AutowiredAnnotationBeanPostProcessor.class, processor, "injectionMetadataCache");
					} else if (processor instanceof CommonAnnotationBeanPostProcessor) {
						disableCache(CommonAnnotationBeanPostProcessor.class, processor, "injectionMetadataCache");
					} else if (processor instanceof PersistenceAnnotationBeanPostProcessor) {
						disableCache(PersistenceAnnotationBeanPostProcessor.class, processor, "injectionMetadataCache");
					}
				}
			}
		}
	}
	
	/**
	 * {@link BeanFactory} の構築後のカスタマイズをおこないます。
	 * Hotdeploy時に Session 情報の再構築をおこなう {@link ReloadableSessionScope} を登録します。
	 * @param beanFactory カスタマイズ対象の {@link BeanFactory}
	 */
	protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
		super.postProcessBeanFactory(beanFactory);
		
    	if (DeploymentMode.isDevelopmentMode()) {
			beanFactory.registerScope(WebApplicationContext.SCOPE_SESSION, new ReloadableSessionScope(false));
			beanFactory.registerScope(WebApplicationContext.SCOPE_GLOBAL_SESSION, new ReloadableSessionScope(true));
		}
	}
	
	/**
	 * 登録されている BeanFactoryPostProssors の処理を実行します。
	 * @see org.springframework.context.support.AbstractApplicationContext#invokeBeanFactoryPostProcessors(org.springframework.beans.factory.config.ConfigurableListableBeanFactory)
	 */
	protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
		String originalBasePackage = System.getProperty(BASE_PACKAGE_KEY);
		try {
			if (basePackage != null) {
				System.setProperty(BASE_PACKAGE_KEY, basePackage);
			}
			super.invokeBeanFactoryPostProcessors(beanFactory);
    	} finally {
    		if (originalBasePackage != null) {
    			System.setProperty(BASE_PACKAGE_KEY, originalBasePackage);
    		} else {
    			System.clearProperty(BASE_PACKAGE_KEY);
    		}
    	}
	}
	
	/**
	 * メッセージ・ソースを初期化します。
	 * 外部から呼び出せるように、メソッドのスコープをpublicに変更するために
	 * オーバーライドしています。
	 */
	public void initMessageSource() {
		super.initMessageSource();
	}
		
	/**
	 * Reflectionを利用して、内部キャッシュを保持しているMap型の
	 * privateフィールドに対して、{@link NoOperationMap}を設定します。 
	 * @param klass 設定対象クラス
	 * @param target 設定対象インスタンス
	 * @param fieldName 設定対象フィールド
	 */
	private void disableCache(Class klass, Object target, String fieldName) {
		Field cache = ReflectionUtils.findField(klass, fieldName);
		if (cache != null) {
			ReflectionUtils.makeAccessible(cache);
			ReflectionUtils.setField(cache, target, new NoOperationMap());
		}
	}
	
}